<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>wenyan-lang Online IDE</title>
  <style>
    body {
      margin: 0px;
      overflow: hidden;
      font-family: sans-serif;
    }
    #in-outer, #js-outer, #ex-outer {
      position: absolute;
    }
    #out-outer {
      position: absolute;
      overflow: hidden;
      font-size: 13px;
    }
    #in, #js, #out, #explorer {
      width: 100%;
      height: calc(100% - 32px);
      overflow: auto;
    }
    #in {
      height: calc(100% - 54px);
    }
    .bar {
      height:20px;
      padding:5px 10px;
      background: #f1f1f1;
      border: 1px solid gainsboro;
      z-index: 1000;
      font-size: 15px;
      overflow: hidden;
      white-space: nowrap;
    }
    .bar .title {
      display: inline-block;
      color: #777;
      padding: 0 10px;
    }
    #current-file-name {
      font-weight: bold;
      padding-right: 10px;
    }
    #out {
      position: absolute;
      padding: 10px;
      margin: 0px;
      top: 30px;
      bottom: 0;
      left: 0;
      right: 0;
      background: white;
      overflow: auto;
      font-family: monospace;
      width: auto;
      height: auto;
      word-break: normal !important;
      word-wrap: normal !important;
      white-space: pre !important;
    }
    #hand-v, #hand-ex {
      position:absolute;
      cursor: ew-resize;
      z-index: 10;
    }
    .editor-hint {
      position: absolute;
      bottom: 0;
      left: 0;
      right: 0;
      background: #f1f1f1;
      z-index: -2;
      padding: 3px 6px;
      font-size: 14px;
      color: #777;
      overflow: hidden;
      white-space: nowrap;
      font-style: italic;
    }
    #hand-h{
      position:absolute;
      cursor: ns-resize;
      z-index: 10;
    }
    #hand-v-inner, #hand-ex-inner{
      background: white;
      border: 1px solid grey;
      box-shadow: 1px 1px 1px rgba(0,0,0,0.1);
      height:calc(100% - 3px);
      margin:0px 3px 3px 3px;
    }
    #hand-h-inner{
      background: white;
      border: 1px solid grey;
      box-shadow: 1px 1px 1px rgba(0,0,0,0.1);
      margin:4px 0px 4px 0px;
      width: 100%;
      height: 1px;
    }
    #explorer {
      overflow-x: hidden;
      background: white;
      padding: 8px 0;
    }
    .caret {
      font-weight: bold;
      cursor: pointer;
      user-select: none;
      opacity: 0.5;
    }
    .caret::before {
      font-size: 10px;
      content: "\25B6";
      color: black;
      display: inline-block;
      margin-left: 8px;
      margin-right: 4px;
      opacity: 0.5;
      transform: scaleX(0.8) translateY(-2px);
    }
    .caret.active::before {
      transform: scaleY(0.8) translateY(-2px) rotate(90deg);
    }
    ul.nested {
      display: none;
      padding: 5px 0;
    }
    ul.nested.active {
      display: block;
    }
    #explorer-list {
      padding: 0;
      margin: 0;
    }
    #explorer-list li, #explorer-list ul {
      list-style: none;
    }
    #explorer-list li {
      padding: 0;
      margin: 0;
      font-size: 13px;
      font-family: sans-serif;
      padding: 3px 0;
      white-space: nowrap;
      cursor: pointer;
    }
    #explorer-list ul.nested li {
      padding: 3px 5px 3px 20px;
    }
    #explorer-list ul.nested li:hover {
      background: #f1f1f1;
    }
    #explorer-list ul.nested li.active {
      background: #ddd;
      font-weight: bold;
    }
    #explorer-list ul.nested li .name {
      padding-right: 5px;
    }
    #explorer-list ul.nested li .alias {
      opacity: 0.5
    }
    button{
      background: white;
      border: 1px solid gainsboro;
      border-radius: 3px;
      margin: 0 3px;
    }
    button:hover{
      background:gainsboro;
      border:1px solid grey;
    }
    button:active{
      background: silver;
    }
    button:focus{
      outline:none;
    }
    button.right-aligned {
      float: right;
    }
    button.hidden {
      display: none;
    }
  </style>
  
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.32.0/codemirror.min.js"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.32.0/codemirror.min.css" />
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.32.0/addon/hint/show-hint.css" />
  <link rel='stylesheet' type='text/css' href='https://cdn.jsdelivr.net/gh/wenyan-lang/highlight/wenyan-light.codemirror.css'/>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.32.0/addon/runmode/runmode.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.32.0/addon/mode/simple.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.32.0/addon/selection/active-line.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.32.0/addon/hint/show-hint.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.32.0/mode/javascript/javascript.js"></script>
  <script src="https://cdn.jsdelivr.net/gh/wenyan-lang/highlight/codemirror.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/file-saver@2.0.2/dist/FileSaver.min.js"></script>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.10.2/beautify.js"></script>
  <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.16.2/build/highlight.min.js"></script>
  <script src="./dist/core.js"></script>
  <script src="./dist/examples.js"></script>
  <script src="./dist/render.js"></script>
</head>

<body>
  <div id="ex-outer">
    <div class="bar" id="explorer-bar">
      <span class="title">Explorer</span>
      <button id="new-file" class="right-aligned">New</button>
    </div>
    <div id="explorer">
      <ul id="explorer-list">
        <li>
          <span class="caret">My Scripts</span>
          <ul class="nested active" id="explorer-list-user">
          </ul>
        </li>
        <li>
          <span class="caret">Examples</span>
          <ul class="nested active" id="explorer-list-examples">
          </ul>
        </li>
      </ul>
    </div>
  </div>

  <div id="in-outer">
    <div class="bar" id="in-bar">
      <span class="title">文言</span>
      <span id="current-file-name"></span>
      <button id="compile">Compile</button>
      <button id="crun">Run</button>
      <button id="delete-current" class="right-aligned">Delete</button>
      <button id="download-current" class="right-aligned">Download</button>
    </div>
    <div id="in"></div>
    <div class="editor-hint" id="in-hint">Hint: Press <code>Ctrl+.</code> or <code>Cmd+.</code> to show autocomplete.</div>
  </div>
  
  <div id="js-outer">
    <div class="bar" id="js-bar">
      <span class="title">Compiled</span>
      &nbsp;&nbsp;
      <small>Romanization</small>&nbsp;<select id="pick-roman"></select>
      &nbsp;&nbsp;&nbsp;
      <input type="checkbox" id="hide-std" checked=""/><small>Hide Imported</small>
      &nbsp;&nbsp;
      <button id="run">Run JS</button>

      <button id="dark" class="right-aligned">Dark Mode</button>
    </div>
    <div id="js" class="cm-s-wenyan-light"></div>
  </div>
  
  <div id="out-outer">
    <div class="bar" id="out-bar">
      <span class="title">Output</span>
      <input type="checkbox" id="output-hanzi" checked=""/><small>Print Hanzi</small>
      &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
      <button id="rend">Render Book</button>
    </div>
    <pre id="out"></pre>
  </div>
  <div id="hand-ex"><div id="hand-ex-inner"></div></div>
  <div id="hand-h"><div id="hand-h-inner"></div></div>
  <div id="hand-v"><div id="hand-v-inner"></div></div>

  <div id="keywords"></div>

  <script>

const LOCAL_STORAGE_KEY = 'wenyang-ide'
const DEFAULT_STATE = ()=> ({config: {}, files: {}})
const EXPLORER_WIDTH_MIN = 0
const EXPLORER_WIDTH_MAX = 400
const EDITOR_WIDTH_MIN = 150
const EDITOR_HEIGHT_MIN = 30
const OUTPUT_HEIGHT_MIN = 30

var handv = window.innerWidth*0.6;
var handh = window.innerHeight*0.7;
var handex = window.innerWidth*0.15;
var djs = document.getElementById("js-outer")
var din = document.getElementById("in-outer")
var dou = document.getElementById("out-outer")
var dex = document.getElementById("ex-outer")

var dhv = document.getElementById("hand-v")
var dhh = document.getElementById("hand-h")
var dhex = document.getElementById("hand-ex")

var exlist = document.getElementById("explorer-list")
var exlistUser = document.getElementById("explorer-list-user")
var exlistExamples = document.getElementById("explorer-list-examples")

var selr = document.getElementById("pick-roman");
var hidestd = document.getElementById("hide-std");
var outputHanzi = document.getElementById("output-hanzi");
var outdiv = document.getElementById("out");
var deleteBtn = document.getElementById("delete-current");
var fileNameSpan = document.getElementById("current-file-name");

var currentFile = {}

var state

var examples = {}

var savingLock = false // to ignore changes made from switching files

for (const key of Object.keys(Examples.examples)) {
  examples[key] = {
    name: key,
    alias: Examples.examplesAlias[key],
    author: 'examples',
    readonly: true,
    code: Examples.examples[key]
  }
}

function loadState() {
  const raw = localStorage.getItem(LOCAL_STORAGE_KEY)
  if (raw)
    state = JSON.parse(raw)
  else 
    state = DEFAULT_STATE()
}

function saveState() {
  localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(state))
}

loadState()

dhv.onmousedown = function(e){
  e.preventDefault();
  var handler = function(event) {
    if (event.buttons === 1){
      let x = event.pageX
      if (x < handex + EDITOR_WIDTH_MIN)
        return
      if (x > window.innerWidth - EDITOR_WIDTH_MIN)
        return
      handv = x;
      setView();
    } else {
      document.body.removeEventListener('mousemove', handler);
    } 
  };
  document.body.addEventListener('mousemove', handler);
}

dhh.onmousedown = function(e){
  e.preventDefault();
  var handler = function(event){
    if (event.buttons === 1){
      let y = event.pageY;
      if (y < EDITOR_HEIGHT_MIN)
        return
      if (y > window.innerHeight - OUTPUT_HEIGHT_MIN)
        return
      handh = y;
      setView();
    }else{
      document.body.removeEventListener('mousemove', handler);
    } 
  };
  document.body.addEventListener('mousemove', handler);
}

dhex.onmousedown = function(e){
  e.preventDefault();
  var handler = function(event){
    if (event.buttons === 1){
      let x = event.pageX
      if (x < EXPLORER_WIDTH_MIN)
        return
      if (x > EXPLORER_WIDTH_MAX)
        return
      if (x > handv - EDITOR_WIDTH_MIN)
        return
      handex = x
      setView();
    }else{
      document.body.removeEventListener('mousemove', handler);
    } 
  };
  document.body.addEventListener('mousemove', handler);
}

function setView(){
  // requestAnimationFrame(setView)
  var W = window.innerWidth;
  var H = window.innerHeight;

  dex.style.left="0px";
  dex.style.top="0px";
  dex.style.width=handex+"px"
  dex.style.height=H+"px";

  din.style.left=handex+"px";
  din.style.top="0px";
  din.style.width=(handv-handex)+"px"
  din.style.height=handh+"px";

  djs.style.left=handv+"px";
  djs.style.top="0px";
  djs.style.width=(W-handv)+"px"
  djs.style.height=handh+"px";

  dou.style.left=handex+"px";
  dou.style.top=handh+"px";
  dou.style.width=(W-handex)+"px"
  dou.style.height=(H-handh)+"px";

  var hw = 9;

  dhex.style.left=(handex-hw/2)+"px";
  dhex.style.top="0px";
  dhex.style.width=hw+"px"
  dhex.style.height=H+"px";
    
  dhv.style.left=(handv-hw/2)+"px";
  dhv.style.top="0px";
  dhv.style.width=hw+"px"
  dhv.style.height=handh+"px";

  dhh.style.left=handex+"px";
  dhh.style.top=(handh-hw/2)+"px";
  dhh.style.width=W+"px"
  dhh.style.height=hw+"px";
}

document.body.onresize = setView
setView()

var editorCM = CodeMirror(document.getElementById("in"), {
  value: '',
  mode: "wenyan",
  lineNumbers: true,
  theme: "wenyan-light",
  styleActiveLine: true,
});

editorCM.setSize(null,"100%");

var jsCM = CodeMirror(document.getElementById("js"), {
  value: '',
  mode:  "javascript",
  lineNumbers: true,
  theme: "wenyan-light",
  styleActiveLine: true,
});

jsCM.setSize(null,"100%");

function hideImportedModules(source) {
  const markerRegex = /\/\*___wenyan_module_([\s\S]+?)_(start|end)___\*\//g;
  const matches = [];

  var match;
  while ((match = markerRegex.exec(source))) {
    if (!match) break;

    if (matches.length) {
      const prev = matches[matches.length - 1];
      if (prev[2] !== "end" && prev[1] !== match[1]) continue; // ignore nested imports
    }

    matches.push(match);
  }

  for (const match of matches) {
    if (match[2] === "start") continue;

    source = source.replace(
      new RegExp(
        `\\/\\*___wenyan_module_${match[1]}_start___\\*\\/[\\s\\S]*\\/\\*___wenyan_module_${match[1]}_end___\\*\\/`
      ),
      `/* module ${match[1]} is hidden */\n`
    );
  }

  return source;
}


function initExplorer() { 
  updateExplorerList()

  var toggler = document.getElementsByClassName("caret");
  var i;

  for (i = 0; i < toggler.length; i++) {
    toggler[i].addEventListener("click", function() {
      this.parentElement.querySelector('.nested').classList.toggle("active");
      this.classList.toggle("active");
    });
  }
}

function updateExplorerList(){
  exlistExamples.innerHTML = '';
  for (let file of Object.values(examples)) {
    exlistExamples.appendChild(createExplorerEntry(file));
  }

  exlistUser.innerHTML = '';
  for (let file of Object.values(state.files)) {
    exlistUser.appendChild(createExplorerEntry(file));
  }
}

function createExplorerEntry({ name, alias }) {
  const item = document.createElement("li");
  item.value = name;

  if (currentFile.name === name)
    item.classList.add('active')

  const a = document.createElement("span")
  const n = document.createElement("span")
  n.classList.add('name')
  n.innerText = alias || name
  item.appendChild(n)
  if (alias) {
    a.classList.add('alias')
    a.innerText = name
    item.appendChild(a)
  }
  item.onclick = () => openFile(name);
  return item
}

function openFile(name) {
  var searchParams = new URLSearchParams(window.location.search)
  if (searchParams.get('file') !== name){
    searchParams.set("file", name)
    history.pushState({ file:name }, name, '?'+searchParams.toString());
  }
  loadFile(name)
}

function loadFile(name) {
  if (currentFile.name !== name) {
    currentFile = state.files[name] || examples[name]
    if (!currentFile){
      currentFile = { name, code: ''}
      state.files[name] = currentFile
      saveState()
    }

    savingLock = true
    editorCM.setValue(currentFile.code || '');
    savingLock = false
    crun();
  }
  fileNameSpan.innerText = currentFile.alias || currentFile.name
  updateExplorerList()
  deleteBtn.classList.toggle('hidden', !!currentFile.readonly)
}

function loadFromUrl(){
  loadFile(new URLSearchParams(location.search).get('file') || 'mandelbrot')
}

function deleteCurrentFile(){
  const yes = confirm(`Are you sure to delete file ${currentFile.alias || currentFile.alias}?\n\nThis operation can NOT be undone.`)
  if (yes) {
    delete state.files[currentFile.name]
    saveState()
    openFile(Object.keys(state.files)[0] || 'mandelbrot')
  }
}

function createNewFile(){
  const name = prompt("New filename", "Untitled");
  if (name)
    openFile(name)
}

function downloadCurrentFile() {
  var blob = new Blob([currentFile.code], {type: "text/wenyan;charset=utf-8"});
  saveAs(blob, `${currentFile.alias || currentFile.name}.wy`);
}

function renameCurrentFile() {
  if (currentFile.readonly)
    return
  const name = prompt("Rename", currentFile.alias || currentFile.name);
  if (name) {
    delete state.files[currentFile.name]
    delete currentFile.alias
    currentFile.name = name
    state.files[name] = currentFile
    saveState()
    openFile(name)
  }
}

const snippets = [
  { text:"「", trigger: "`" },
  { text:"」", trigger: "'"},
  { text:"「「", trigger: '['},
  { text:"」」", trigger: "]"},
  ...Object.keys(Wenyan.KEYWORDS)
    .map(x=>({
      text: x, 
      trigger: x.length > 1
        ? Wenyan.hanzi2pinyin(x).replace(/([A-z])[A-z]+?([1-9])/g,"$1")
        : Wenyan.hanzi2pinyin(x)
    }))
]

snippets.sort((a,b)=>a.trigger.localeCompare(b.trigger))
snippets.forEach(s => {
  s.displayText = s.trigger + " • " + s.text
})

function showHint() {
  CodeMirror.showHint(editorCM, () => {
    const cursor = editorCM.getCursor()
    const token = editorCM.getTokenAt(cursor)
    console.log(cursor, token)
    const start = token.start
    const end = cursor.ch
    const line = cursor.line
    const currentWord = token.string

    // TODO: filter hints
    /*
    const list = snippets.filter((item) => {
      return item.indexOf(currentWord) >= 0
    })
    */

    return {
      list: snippets,
      from: CodeMirror.Pos(line, end),
      to: CodeMirror.Pos(line, end)
    }
  }, { completeSingle: false })
}

editorCM.setOption('extraKeys', {
  'Cmd-.': showHint,
  'Ctrl-.': showHint
})

editorCM.on('change', (e)=>{
  if (savingLock)
    return

  if (!currentFile.readonly) {
    currentFile.code = editorCM.getValue()
    saveState()
  }
  else {
    // make a copy for examples
    let num = 1
    while (state.files[`${currentFile.name}_${num}`]){
      num += 1
    }
    const name = `${currentFile.name}_${num}`
    const newFile = {
      name: name,
      alias: currentFile.alias? `${currentFile.alias}「${Wenyan.num2hanzi(num)}」` : '',
      code: editorCM.getValue()
    }
    state.files[name] = newFile
    currentFile = newFile
    saveState()
    openFile(name)
  }
})


window.addEventListener('popstate', loadFromUrl)

loadFromUrl()

initExplorer()

for (var k of ["none", "pinyin", "baxter", "unicode"]) {
  var opt = document.createElement("option");
  opt.value = k;
  opt.innerHTML = k;
  selr.appendChild(opt);
}
selr.value = "pinyin";
selr.onchange = crun;

hidestd.onchange = crun;
outputHanzi.onchange = crun;

function compile(){
  document.getElementById("out").innerText = "";
  var log = ""
  var code = Wenyan.compile(editorCM.getValue(), {
    lang: "js",
    romanizeIdentifiers: selr.value,
    resetVarCnt: true,
    errorCallback: (...args) => (outdiv.innerText += args.join(" ") + "\n"),
    reader: x => Examples.examples[x],
    logCallback:(x)=>{log+=x+"\n"},
    strict:true
  });
  var sig = log.split("=== [PASS 2.5] TYPECHECK ===\n")[1].split("=== [PASS 3] COMPILER ===")[0];
  outdiv.innerText = sig;
  var showcode = hidestd.checked ? hideImportedModules(code) : code;
  jsCM.setValue(js_beautify(showcode));
}

function run() {
  document.getElementById("out").innerText = "";
  Wenyan.evalCompiled(jsCM.getValue(), {
    outputHanzi: outputHanzi.checked,
    output: (...args) => {
      outdiv.innerText += args.join(" ") + "\n";
    }
  });
}

function crun(){
  document.getElementById("out").innerText = "";
  var code = Wenyan.compile(editorCM.getValue(), {
    lang: "js",
    romanizeIdentifiers: selr.value,
    resetVarCnt: true,
    errorCallback: (...args) => (outdiv.innerText += args.join(" ") + "\n"),
    reader: x => Examples.examples[x]
  });

  var showcode = hidestd.checked ? hideImportedModules(code) : code;

  // CodeMirror.runMode(js_beautify(showcode),"JavaScript",document.getElementById("js"));

  jsCM.setValue(js_beautify(showcode));

  try {
    Wenyan.evalCompiled(code, {
      outputHanzi: outputHanzi.checked,
      output: (...args) => {
        outdiv.innerText += args.join(" ") + "\n";
      }
    });
  }catch(e){
    console.error(e)
  }
}

document.getElementById("compile").onclick = compile;
document.getElementById("run").onclick = run;
document.getElementById("crun").onclick = crun;
document.getElementById("new-file").onclick = createNewFile;
document.getElementById("download-current").onclick = downloadCurrentFile;
deleteBtn.onclick = deleteCurrentFile
fileNameSpan.onclick = renameCurrentFile
crun();

// document.getElementById("insert").onmousedown= function(){
//   var doc = editorCM.getDoc();
//   var cursor = doc.getCursor();
//   doc.replaceRange("hi", cursor);
//   return false;
// }

var isDark = false;
document.getElementById("dark").onclick = function(){
  if (!isDark){
    document.getElementById("dark").innerHTML = "Light Mode"
    document.body.style.filter="invert(0.88)";
  }else{
    document.getElementById("dark").innerHTML = "Dark Mode"
    document.body.style.filter="invert(0)";
  }
  isDark = !isDark;
}
document.getElementById("rend").onclick = function(){
  document.getElementById("out").innerText = "";
  var txt = editorCM.getValue();
  var title;
  try{
    title = txt.split(/吾有一術.?名之曰「/)[1].split("」")[0]
  }catch(e){
    try{
      title = txt.split("「「")[1].split("」」")[0]
    }catch(e){
      try{
        title = txt.split("「")[1].split("」")[0]
      }catch(e){
        title = txt.split("。")[0]
      }
    }
  }
  var svgs = Render.render(title,txt);
  for (var i = 0; i < svgs.length; i++){
    document.getElementById("out").innerHTML += svgs[i]+"<br>";
  }
}
</script>
</body>
</html>
